from numpy import  *
import numpy as np
import cv2
import os
import math
import pickle
import MyThread
# 为了让LBP具有旋转不变性，将二进制串进行旋转。
# 假设一开始得到的LBP特征为10010000，那么将这个二进制特征，
# 按照顺时针方向旋转，可以转化为00001001的形式，这样得到的LBP值是最小的。
# 无论图像怎么旋转，对点提取的二进制特征的最小值是不变的，
# 用最小值作为提取的LBP特征，这样LBP就是旋转不变的了。

def minBinary(pixel):
    '''
    基于旋转不变性的理念  取8种LBP二进制串组合的最小那个作为LBP特征
    :param pixel: LBP特征串
    :return:  最小LBP特征(2进制字符串)
    '''
    length = len(pixel)
    min_lbp = int(pixel, 2)    # 默认LBP特征值
    min_binary = pixel
    for i in range(1, length):     # 求8个旋转LBP 的最小值
        temp_lbp = pixel[i:] + pixel[0:i]
        if int(temp_lbp, 2) < min_lbp:
            min_lbp = int(temp_lbp, 2)  # 最小LBP特征更新
            min_binary = temp_lbp   # 最小LBP特征对应二进制字符串
    return min_binary


# 加载图像
def loadImageSet(imgs_path):  # imgs_path是训练图像文件夹路径
    '''

    :param add:
    :return: 加载指定文件夹的所有jpg图像文件存放在一个矩阵里 (每张图像存在一行) 返回该汇总图像矩阵
    '''
    print("加载图像")
    FaceMat = mat(zeros((15, 116 * 98)))  # 单个训练样本的大小
    j = 0
    for i in os.listdir(imgs_path):
        if i.split('.')[1] == 'jpg':
            try:
                img_path = imgs_path + r"/"+i
                img = cv2.imdecode(np.fromfile(img_path, dtype=np.uint8), cv2.IMREAD_UNCHANGED)  # 图像加载成矩阵
                img = cv2.resize(img, (98, 116))    # 缩放成指定大小
            except:
                print('load %s failed' % i)
            FaceMat[j, :] = mat(img).flatten()      # 转成矩阵行向量  每行都是一个图像  训练集图像有多少张就有多少行 (后面会转置)
            j += 1

    return FaceMat


# 算法主过程
# 圆形LBP算子 半径为2  采样点数是8
def LBP(FaceMat, R=2, P=8):
    '''

    :param FaceMat: 图像矩阵汇总 每列为一个图像
    :param R: 圆形LBP算子的半径
    :param P: 圆形LBP算子的采样个数
    :return:  每个图像的LBP特征值汇总 同样按列排放
    '''

    print("计算图像LBP特征值")
    pi = math.pi
    LBPoperator = mat(zeros(shape(FaceMat)))
    for i in range(shape(FaceMat)[1]):
        # 对每一个图像进行处理  转化成116*98的二维矩阵
        face = FaceMat[:, i].reshape(116, 98)

        W, H = shape(face)
        tempface = mat(zeros((W, H)))
        for x in range(R, W - R):  # x y 是圆心坐标
            for y in range(R, H - R):
                repixel = ''
                pixel = int(face[x, y])  # 取每一个值
                # 　圆形LBP算子
                for p in [2, 1, 0, 7, 6, 5, 4, 3]:  # 遍历各个邻域像素点 xp yp   采用的是半径为2 8邻域 圆形LBP算子
                    p = float(p)
                    xp = x + R * cos(2 * pi * (p / P))
                    yp = y - R * sin(2 * pi * (p / P))

                    if int(face[int(xp), int(yp)]) > pixel:  # pixel是中心像素点 邻域像素点大于该值标记为1 最终组成长度为8的二进制字符串 repixel
                        repixel += '1'
                    else:
                        repixel += '0'
                # minBinary保持LBP算子旋转不变
                tempface[x, y] = int(minBinary(repixel), base=2)    # 得到该区域最小LBP算子
        LBPoperator[:, i] = tempface.flatten().T  # 整个图像的LBP特征值转成列向量  汇总成矩阵(每列为该图像的LBP特征值)
    return LBPoperator




def judgeFace(judgeImg, exHistograms):
    '''
    原理： 每个样本库(比如张三15张自拍构成一个训练样本集)的每张图像训练后都会生成一个LBP特征值统计直方图矩阵 (256*4*7) X 1大小)

    :param judgeImg: 待识别图像矩阵
    :param LBPoperator: 样本库对应的LBP特征值列向量
    :param exHistograms: 样本库每个图像LBP特征的直方图统计信息(就是LBP特征值统计分布 )
    :return:
    '''

    judgeImg = judgeImg.T
    ImgLBPope = LBP(judgeImg)  # 待识别图像的LBP特征值列向量
    #  把图片分为7*4份 , calHistogram返回的直方图矩阵有28个小矩阵内的直方图
    judgeHistogram = calHistogram(ImgLBPope)    # 计算LBP特征向量的统计直方图(一个列向量256*4*7大小)

    sum_diff = []  # 测试图像LBP特征统计直方图和样本库所有图像的BP特征统计直方图之差的和
    for i in range(shape(exHistograms)[1]):
        exHistogram = exHistograms[:, i]
        diff = np.sqrt((array(exHistogram - judgeHistogram) ** 2).sum())  # 欧式距离
        sum_diff.append(diff)

    return np.argsort(sum_diff)[0]   # 返回最小的那个误差


# 统计直方图  用于统计LBP特征值信息  256是因为刚好8个采样点对应的LBP特征值最大为255 最小为0
def calHistogram(ImgLBPope):

    print("统计LBP特征直方图信息")

    Img = ImgLBPope.reshape(116, 98)
    W, H = shape(Img)
    # 把图片分为7*4份
    Histogram = mat(zeros((256, 7 * 4)))
    maskx, masky = W / 4, H / 7  # 29 14
    for i in range(4):
        for j in range(7):
            # 使用掩膜opencv来获得子矩阵直方图
            mask = zeros(shape(Img), uint8)
            mask[int(i * maskx): int((i + 1) * maskx), int(j * masky):int((j + 1) * masky)] = 255
            hist = cv2.calcHist([array(Img, uint8)], [0], mask, [256], [0, 256])
            Histogram[:, (i + 1) * (j + 1) - 1] = mat(hist).flatten().T
    return Histogram.flatten().T


def runLBP(tuPianPath, allLBPoperator, allexHistograms):
    '''

    :param tuPianPath: 单个训练样本路径
    :param allLBPoperator:  用来装LBP特征库的容器
    :param allexHistograms: 用来装LBP特征库统计直方图的容器
    :return:
    '''
    # 加载图像
    FaceMat = loadImageSet(tuPianPath).T  # 反转矩阵  反转后每列都是一个图像
    LBPoperator = LBP(FaceMat)  # 获得样本图像LBP算子

    # 获得实验图像的直方图分布，这里计算是为了可以多次使用

    exHistograms = mat(zeros((256 * 4 * 7, shape(LBPoperator)[1])))   # 每一列为 256*4*7
    for i in range(shape(LBPoperator)[1]):
        exHistogram = calHistogram(LBPoperator[:, i])
        exHistograms[:, i] = exHistogram
    allLBPoperator.append(LBPoperator)  # 存好训练模型 LBP算子
    allexHistograms.append(exHistograms)

def get_name_from_face(allmen, allexHistograms, face):  # face是灰度图像 size为98*116
    '''

    :param allmen: 训练样本库的类名  (员工姓名列表)  #标签数组
    :param allexHistograms:
    :param face:
    :return:识别的人脸名字
    '''

    result = inf  # 初始化待检测人类与样本库的最小差异值 (寻找最小差异值对应的样本即为识别结果)
    show_name = ''
    for i in range(len(allmen)):  # 找差异最小的样本
        jresult = judgeFace(mat(face).flatten(), allexHistograms[i])  # 计算与该样本的差异值
        if jresult < result:
            result = jresult
            show_name = allmen[i]
    return show_name


#训练识别模型
def get_face_model(train_names):    #  训练样本库的类名 (员工姓名列表)
    '''

    :return: 提取样本LBP特征，返回特征比对库
    '''
    allLBPoperator = []
    allexHistograms = []  # LBP算子和直方图算子
    BASE_DIR = os.path.dirname(__file__)  #获取当前文件夹的绝对路径
    allTuPianPath = []
    for ii in range(len(train_names)):
        allTuPianPath.append(BASE_DIR+r'/face_project/train/'+train_names[ii]) # 训练样本路径 （样本标签作为文件夹名）

    for tuPianPath in allTuPianPath:  # 单个样本库训练 每训练完一个样本把LBP特征库和LBP特征统计分布库存入
        runLBP(tuPianPath, allLBPoperator, allexHistograms)
    value = [allLBPoperator, allexHistograms]   # 保存
    pickle.dump(value, open(BASE_DIR+'/'+r'train_model.txt', 'wb'))
    return allLBPoperator, allexHistograms





def main_fun(allmen):
    '''
    :param allmen:  训练样本库的类名  (员工姓名列表)  #标签数组
     调摄像头 识别人脸后显示人名
    :return:
    '''

    count = 0   # 总识别次数
    req_count = {}
    for name in allmen:   # 统计识别次数 初始均为0
        req_count[name] = 0


    BASE_DIR = os.path.dirname(__file__)  # 获取当前文件夹的绝对路径
    models = pickle.load(open(BASE_DIR+r'/train_model.txt', 'rb'))
    allLBPoperator, allexHistograms =models[0], models[1] # 获得LBP算子和直方图算子

    cv2.namedWindow("Capture_Test")
    cap = cv2.VideoCapture(0)  # 创建一个 VideoCapture 对象
    cap.set(4, 720)

    flag = 1  # 设置一个标志，用来输出视频信息
    """
    函数名：cv2.isOpened()
    功  能：返回一个布尔值（ True / False ），检查是否初始化成功，成功返回 True
    返回值：布尔值
    """
    classfier = cv2.CascadeClassifier("haarcascade_frontalface_default.xml")
    color = (0, 255, 0)




    while True:  # 循环读取每一帧
        """
        函数名：cap.read()
        功  能：返回两个值
               先返回一个布尔值，如果视频读取正确，则为 True，如果错误，则为 False，也可用来判断是否到视频末尾
               再返回一个值，为每一帧的图像，该值是一个三维矩阵
               通用接收方法为：
               ret,frame = cap.read();
               这样 ret 存储布尔值，frame 存储图像
               若使用一个变量来接收两个值，如
               frame = cap.read()
               则 frame 为一个元组，原来使用 frame 处需更改为 frame[1]
        返回值：R1：布尔值
               R2：图像的三维矩阵
        """
        ret_flag, Vshow = cap.read()
        divisor = 8
        h, w = np.shape(Vshow)[:2]  # 整个图像的 行 列
        minSize = (w // divisor, h // divisor)

        if ret_flag:
            #人脸检测
            grey = cv2.cvtColor(Vshow, cv2.COLOR_BGR2GRAY)
            faceRects = classfier.detectMultiScale(grey, 1.2, 2, cv2.CASCADE_SCALE_IMAGE, minSize)  # 人脸检测

            if len(faceRects) > 0:  #检测到了人脸继续识别
                for faceRect in faceRects:  #可能多个人脸
                    x, y, w, h = faceRect

                    cv2.rectangle(Vshow, (x,y ), (x + w, y + h), color, 3)
                    face = np.zeros((h, w), dtype='uint8')  #初始化一个人脸空矩阵

                    for ii in range(x,x+w):
                        for jj in range(y,y+h):
                            face[jj - y, ii - x] = grey[jj, ii]   #抠下检测出的人脸

                    face = cv2.resize(face, (116, 98))   # resize成指定大小  和训练时一致


                    mt = MyThread.MyThread()  #为了不卡 多线程运行
                    g_func_list = []
                    g_func_list.append({"func": get_name_from_face, "args": (allmen, allexHistograms, face)})
                    mt.set_thread_func_list(g_func_list)
                    mt.start()
                    if size(mt.ret_flag) > 0:
                        count += 1
                        show_name = mt.ret_flag[0]
                        req_count[show_name] += 1  # 识别结果统计  用于计算正确率
                        print(show_name)
                        cv2.putText(Vshow, show_name, (x, y - 20), cv2.FONT_HERSHEY_SIMPLEX, 1, 255, 2)  # 显示名字
        cv2.imshow("Capture_Test", Vshow)

        k = cv2.waitKey(1) & 0xFF  # 每帧数据延时 1ms，延时不能为 0，否则读取的结果会是静态帧
        if k == ord('s'):  # 若检测到按键 ‘s’，打印字符串
            """
            函数名：cap.get( propId )
            功  能：查询视频的参数
            返回值：无
            参数一：查询的视频参数，其中部分值可以使用 cap.set() 进行修改
            """
            print(cap.get(3))
            print(cap.get(4))

        elif k == ord('q'):  # 若检测到按键 ‘q’，退出
            for key in req_count:
                print("%s识别次数为 %d 识别率为 %f" % (key, req_count[key], req_count[key]/count))
            break
    # print(names)
    cap.release()  # 释放摄像头
    cv2.destroyAllWindows()  # 删除建立的全部窗口




